﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace LeftFork.TestHelpers {

    public static class SpecExtensions {

        class Metadata {
            public int Hash { get; set; }
            public bool Not { get; set; }
        }
        static List<Metadata> _list = new List<Metadata>();

        public static T should<T>(this T src) {
            if (src == null)
                return default(T);

            var meta = _list.FirstOrDefault(m => m.Hash == src.GetHashCode());
            if (meta == null) {
                meta = new Metadata() { Hash = src.GetHashCode() };
                _list.Add(meta);
            }
            meta.Not = false;
            return src;
        }

        public static T not<T>(this T src) {
            if (src == null)
                throw new Exception("This function not available to null object");

            var meta = _list.FirstOrDefault(m => m.Hash == src.GetHashCode());
            if (meta == null) {
                meta = new Metadata() { Hash = src.GetHashCode() };
                _list.Add(meta);
            }
            meta.Not = true;
            return src;
        }

        public static bool GetNot(object src) {

            var meta = _list.FirstOrDefault(m => m.Hash == src.GetHashCode());

            return meta == null ? false : meta.Not;
        }

        public static T equal<T>(this T src, T compareTo) {

            return src.equal(compareTo, "Two objects are not equal", "Two objects are equal");
        }

        public static T equal<T>(this T src, T compareTo, string failShouldMessage, string failShouldNotMessage) {
            if (src == null) {
                Assert.IsNull(compareTo);
                return default(T);
            }
            bool not = GetNot(src);
            if (not)
                Assert.AreNotEqual<T>(compareTo, src, failShouldNotMessage);
            else
                Assert.AreEqual<T>(compareTo, src, failShouldMessage);
            return src;
        }

        public static T ParamRequired<T>(this T src) {
            if (src == null)
                throw new ArgumentException("Missing argument");
            if (src is string)
                if (string.IsNullOrWhiteSpace(src as string))
                    throw new ArgumentException("Missing argument");
            return src;
        }

        public static int be_less_than(this int src, int other) {
            bool not = GetNot(src);
            if (not) {
                if (src < other)
                    throw new ArgumentException("Bad argument");
            } else {
                if (src >= other)
                    throw new ArgumentException("Bad argument");
            }
            return src;
        }

        public static T be_instance_of<T>(this T src) {
            if (src == null) {
                Assert.Fail("Null value supplied for instance check");
                return default(T);
            }

            bool not = GetNot(src);
            if (not)
                Assert.IsNotInstanceOfType(src, typeof(T));
            else
                Assert.IsInstanceOfType(src, typeof(T));

            return src;
        }

        public static void be_null(this object src) {
            Assert.IsNull(src);
        }

        public static T with<T>(this T src, Action<T> propSetter) {
            propSetter.Invoke(src);
            return src;
        }

        public static T not_be_null<T>(this T src) {
            Assert.IsNotNull(src);
            return src;
        }

        public static T have_equal_members<T>(this T src, params KVP[] members) where T : class {
            if (src == null)
                Assert.Fail("Value is null");

            var classFields = src.GetType().GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
            var classProps = src.GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);

            foreach (var m in members) {
                var c = classFields.SingleOrDefault(s => s.Name == m.Key);
                if (c != null) {
                    Assert.AreEqual(m.Value, c.GetValue(src), "Field " + m.Key + " has an incorrect value");
                } else {
                    var p = classProps.SingleOrDefault(s => s.Name == m.Key);
                    if (p != null)
                        Assert.AreEqual(m.Value, p.GetValue(src, null), "Property " + m.Key + " has an incorrect value");
                    else
                        Assert.Fail("Member " + m.Key + " not found");
                }
            }

            return src;
        }

        public static object get<T>(this T src, string name) where T : class {
            var classFields = typeof(T).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
            var m = classFields.SingleOrDefault(s => s.Name == name);
            if (m != null)
                return m.GetValue(src);

            var classProps = typeof(T).GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
            var p = classProps.SingleOrDefault(s => s.Name == name);
            if (p != null)
                return p.GetValue(src, null);

            throw new ArgumentException("Not found", "name");
        }

        public static T set<T>(this T src, string name, object value) where T : class {
            var classFields = typeof(T).GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
            var m = classFields.SingleOrDefault(s => s.Name == name);
            if (m != null) {
                m.SetValue(src, value);
                return src;
            }

            var classProps = typeof(T).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
            var p = classProps.SingleOrDefault(s => s.Name == name);
            if (p != null) {
                p.SetValue(src, value, null);
                return src;
            }

            throw new ArgumentException("Not found", "name");

        }

        public static object call<T>(this T src, string name) where T : class {
            return call(src, name, new object[] { });
        }

        public static object call<T>(this T src, string name, params object[] parameters) where T : class {
            var methods = typeof(T).GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
            var m = methods.SingleOrDefault(s => s.Name == name);
            if (m != null) {
                try {
                    return m.Invoke(src, parameters);
                } catch (Exception ex) {
                    throw ex.InnerException;
                }
            }

            throw new ArgumentException("Not found", "name");
        }

        public static T callVoid<T>(this T src, string name) where T : class {
            return callVoid(src, name, new object[] { });
        }

        public static T callVoid<T>(this T src, string name, params object[] parameters) where T : class {
            var methods = typeof(T).GetMethods(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
            var m = methods.SingleOrDefault(s => s.Name == name);
            if (m != null) {
                try {
                    m.Invoke(src, parameters);
                    return src;
                } catch (Exception ex) {
                    throw ex.InnerException;
                }
            }

            throw new ArgumentException("Not found", "name");
        }
    }

    public class KVP {
        public KVP() { }
        public KVP(string key, object value) {
            this.Key = key;
            this.Value = value;
        }

        public string Key { get; set; }
        public object Value { get; set; }
    }

}
